Jerry's Log

How java app runs

contents

이 과정은 소스 코드(Source Code)바이트코드(Bytecode) 로 변환되고, 최종적으로 JVM(Java Virtual Machine) 내부에서 네이티브 기계어(Native Machine Code) 로 바뀌는 여정입니다.

실행 수명 주기를 단계별로 나누어 보겠습니다.

1단계: 컴파일 타임 (javac 프로세스)

애플리케이션이 실행되기 전, 변환 과정이 필요합니다. CPU는 기계어(0과 1)만 이해하지만, Java는 먼저 바이트코드라는 중간 포맷으로 컴파일됩니다.

  1. 어휘 분석 (Lexical Analysis): 컴파일러가 .java 파일을 읽고 토큰(키워드 public, class, 식별자, 연산자 등) 단위로 분해합니다.
  2. 구문 및 의미 분석 (Syntax & Semantic Analysis): 추상 구문 트리(AST)를 생성하여 자바 문법 규칙을 따르는지 확인합니다 (예: 변수 선언 후 사용 여부 확인).
  3. 바이트코드 생성 (Bytecode Generation): 컴파일러가 AST를 바이트코드(16진수 opcode)가 담긴 .class 파일로 변환합니다.
    • 참고: 이 코드는 플랫폼 독립적입니다. 아직 윈도우에서 실행될지 리눅스에서 실행될지 모르는 상태입니다.

2단계: JVM 시작 및 로딩

java com.example.MyProject 명령어를 실행하면 운영체제(OS)가 JVM을 시작합니다.

1. 클래스로더 서브시스템 (ClassLoader Subsystem)

JVM은 .class 파일을 메모리로 가져와야 합니다. 이때 위임 모델(Delegation Model) 을 사용합니다:

2. 링킹 (Linking)

로딩된 클래스는 세 가지 중요한 단계를 거칩니다:

3. 초기화 (Initialization)

로딩의 마지막 단계입니다. JVM은 <clinit> 메서드(클래스 초기화)를 실행합니다.


3단계: 메모리 할당 (런타임 데이터 영역)

main 메서드가 시작되기 전, JVM은 메모리 영역을 배치합니다:

메모리 영역 설명
메서드 영역 (Method Area) 클래스 구조(메타데이터), 상수, static 변수가 저장됩니다.
힙 (Heap) **객체(Object)**가 사는 곳입니다. new Object()를 하면 여기 생성됩니다.
스택 (Stack) 지역 변수와 메서드 호출을 저장합니다. 각 스레드는 자신만의 스택을 가집니다.
PC 레지스터 현재 실행 중인 명령어 주소를 추적합니다.
네이티브 스택 네이티브 C/C++ 메서드(JNI) 실행에 사용됩니다.

4단계: main 메서드 실행

이제 JVM이 진입점(Entry point)을 실행할 준비가 되었습니다.

1. 스택 프레임 생성

JVM은 새로운 스레드(메인 스레드)를 생성하고 public static void main(String[] args)를 찾습니다.

2. 실행 엔진 (Execution Engine)

JVM은 바이트코드 명령어를 하나씩 읽습니다. 실행에는 하이브리드 방식을 사용합니다:

3. 실행 예시

만약 main 메서드에 int a = 10 + 20;이 있다면, 엔진은 다음과 같이 수행합니다:

  1. bipush 10: 10을 오퍼랜드 스택(Operand Stack) 에 넣습니다.
  2. bipush 20: 20을 오퍼랜드 스택에 넣습니다.
  3. iadd: 위 두 값을 꺼내 CPU의 ALU(산술 논리 장치)로 더한 뒤, 결과(30)를 다시 스택에 넣습니다.
  4. istore_1: 결과를 꺼내 지역 변수 배열 1번 인덱스(변수 a)에 저장합니다.

5단계: 정리 및 종료

  1. 가비지 컬렉션 (GC): 프로그램이 실행되면서 힙(Heap)에 생성된 객체 중 더 이상 참조되지 않는 것은 가비지 컬렉터(데몬 스레드)가 메모리를 회수합니다.
  2. 종료 (Exit): 다음 조건이 되면 JVM은 실행을 멈춥니다:
    • main 메서드가 끝났을 때.
    • 모든 사용자 스레드(비-데몬 스레드)가 종료되었을 때.
    • System.exit()이 호출되었을 때.

이 시점에 JVM은 종료되며 메모리를 운영체제에 반환합니다.


요약 흐름

  1. 소스 (.java) -> 컴파일러 -> 바이트코드 (.class)
  2. OSJVM 시작
  3. 클래스로더가 클래스를 찾아 로딩
  4. 검증기(Verifier) 가 코드 안전성 확인
  5. Static 초기화로 static 변수 설정
  6. 메인 스레드 생성 -> 스택 프레임 Push
  7. 인터프리터가 코드 실행 -> JIT가 핫스팟을 네이티브 코드로 최적화
  8. 프로그램 종료 -> JVM 종료

메서드 간에 객체를 주고받을 때 스택(Stack)과 힙(Heap)이 어떻게 상호작용하는지에 대해서도 설명해 드릴까요?

references